// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <limits>
#include <string>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/hash.h"
#include "base/process/process_metrics.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/test/perf_time_logger.h"
#include "base/test/test_file_util.h"
#include "base/threading/thread.h"
#include "net/base/cache_type.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/base/test_completion_callback.h"
#include "net/disk_cache/blockfile/backend_impl.h"
#include "net/disk_cache/blockfile/block_files.h"
#include "net/disk_cache/disk_cache.h"
#include "net/disk_cache/disk_cache_test_base.h"
#include "net/disk_cache/disk_cache_test_util.h"
#include "net/disk_cache/simple/simple_backend_impl.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/platform_test.h"

using base::Time;

namespace {

size_t MaybeGetMaxFds()
{
#if defined(OS_POSIX)
    return base::GetMaxFds();
#else
    return std::numeric_limits<size_t>::max();
#endif
}

void MaybeSetFdLimit(unsigned int max_descriptors)
{
#if defined(OS_POSIX)
    base::SetFdLimit(max_descriptors);
#endif
}

struct TestEntry {
    std::string key;
    int data_len;
};

class DiskCachePerfTest : public DiskCacheTestWithCache {
public:
    DiskCachePerfTest()
        : saved_fd_limit_(MaybeGetMaxFds())
    {
        if (saved_fd_limit_ < kFdLimitForCacheTests)
            MaybeSetFdLimit(kFdLimitForCacheTests);
    }

    ~DiskCachePerfTest() override
    {
        if (saved_fd_limit_ < kFdLimitForCacheTests)
            MaybeSetFdLimit(kFdLimitForCacheTests);
    }

protected:
    enum class WhatToRead {
        HEADERS_ONLY,
        HEADERS_AND_BODY,
    };

    // Helper methods for constructing tests.
    bool TimeWrite();
    bool TimeRead(WhatToRead what_to_read, const char* timer_message);
    void ResetAndEvictSystemDiskCache();

    // Complete perf tests.
    void CacheBackendPerformance();

    const size_t kFdLimitForCacheTests = 8192;

    const int kNumEntries = 1000;
    const int kHeadersSize = 800;
    const int kBodySize = 256 * 1024 - 1;

    std::vector<TestEntry> entries_;

private:
    const size_t saved_fd_limit_;
};

// Creates num_entries on the cache, and writes kHeaderSize bytes of metadata
// and up to kBodySize of data to each entry.
bool DiskCachePerfTest::TimeWrite()
{
    // TODO(gavinp): This test would be significantly more realistic if it didn't
    // do single reads and writes. Perhaps entries should be written 64kb at a
    // time. As well, not all entries should be created and written essentially
    // simultaneously; some number of entries in flight at a time would be a
    // likely better testing load.
    scoped_refptr<net::IOBuffer> buffer1(new net::IOBuffer(kHeadersSize));
    scoped_refptr<net::IOBuffer> buffer2(new net::IOBuffer(kBodySize));

    CacheTestFillBuffer(buffer1->data(), kHeadersSize, false);
    CacheTestFillBuffer(buffer2->data(), kBodySize, false);

    int expected = 0;

    MessageLoopHelper helper;
    CallbackTest callback(&helper, true);

    base::PerfTimeLogger timer("Write disk cache entries");

    for (int i = 0; i < kNumEntries; i++) {
        TestEntry entry;
        entry.key = GenerateKey(true);
        entry.data_len = rand() % kBodySize;
        entries_.push_back(entry);

        disk_cache::Entry* cache_entry;
        net::TestCompletionCallback cb;
        int rv = cache_->CreateEntry(entry.key, &cache_entry, cb.callback());
        if (net::OK != cb.GetResult(rv))
            break;
        int ret = cache_entry->WriteData(
            0, 0, buffer1.get(), kHeadersSize,
            base::Bind(&CallbackTest::Run, base::Unretained(&callback)), false);
        if (net::ERR_IO_PENDING == ret)
            expected++;
        else if (kHeadersSize != ret)
            break;

        ret = cache_entry->WriteData(
            1, 0, buffer2.get(), entry.data_len,
            base::Bind(&CallbackTest::Run, base::Unretained(&callback)), false);
        if (net::ERR_IO_PENDING == ret)
            expected++;
        else if (entry.data_len != ret)
            break;
        cache_entry->Close();
    }

    helper.WaitUntilCacheIoFinished(expected);
    timer.Done();

    return expected == helper.callbacks_called();
}

// Reads the data and metadata from each entry listed on |entries|.
bool DiskCachePerfTest::TimeRead(WhatToRead what_to_read,
    const char* timer_message)
{
    scoped_refptr<net::IOBuffer> buffer1(new net::IOBuffer(kHeadersSize));
    scoped_refptr<net::IOBuffer> buffer2(new net::IOBuffer(kBodySize));

    CacheTestFillBuffer(buffer1->data(), kHeadersSize, false);
    CacheTestFillBuffer(buffer2->data(), kBodySize, false);

    int expected = 0;

    MessageLoopHelper helper;
    CallbackTest callback(&helper, true);

    base::PerfTimeLogger timer(timer_message);

    for (int i = 0; i < kNumEntries; i++) {
        disk_cache::Entry* cache_entry;
        net::TestCompletionCallback cb;
        int rv = cache_->OpenEntry(entries_[i].key, &cache_entry, cb.callback());
        if (net::OK != cb.GetResult(rv))
            break;
        int ret = cache_entry->ReadData(
            0, 0, buffer1.get(), kHeadersSize,
            base::Bind(&CallbackTest::Run, base::Unretained(&callback)));
        if (net::ERR_IO_PENDING == ret)
            expected++;
        else if (kHeadersSize != ret)
            break;

        if (what_to_read == WhatToRead::HEADERS_AND_BODY) {
            ret = cache_entry->ReadData(
                1, 0, buffer2.get(), entries_[i].data_len,
                base::Bind(&CallbackTest::Run, base::Unretained(&callback)));
            if (net::ERR_IO_PENDING == ret)
                expected++;
            else if (entries_[i].data_len != ret)
                break;
        }

        cache_entry->Close();
    }

    helper.WaitUntilCacheIoFinished(expected);
    timer.Done();

    return (expected == helper.callbacks_called());
}

TEST_F(DiskCachePerfTest, BlockfileHashes)
{
    int seed = static_cast<int>(Time::Now().ToInternalValue());
    srand(seed);

    base::PerfTimeLogger timer("Hash disk cache keys");
    for (int i = 0; i < 300000; i++) {
        std::string key = GenerateKey(true);
        base::Hash(key);
    }
    timer.Done();
}

void DiskCachePerfTest::ResetAndEvictSystemDiskCache()
{
    base::RunLoop().RunUntilIdle();
    cache_.reset();

    // Flush all files in the cache out of system memory.
    const base::FilePath::StringType file_pattern = FILE_PATH_LITERAL("*");
    base::FileEnumerator enumerator(cache_path_, true /* recursive */,
        base::FileEnumerator::FILES, file_pattern);
    for (base::FilePath file_path = enumerator.Next(); !file_path.empty();
         file_path = enumerator.Next()) {
        ASSERT_TRUE(base::EvictFileFromSystemCache(file_path));
    }
#if defined(OS_LINUX)
    // And, cache directories, on platforms where the eviction utility supports
    // this (currently Linux only).
    if (simple_cache_mode_) {
        ASSERT_TRUE(
            base::EvictFileFromSystemCache(cache_path_.AppendASCII("index-dir")));
    }
    ASSERT_TRUE(base::EvictFileFromSystemCache(cache_path_));
#endif

    DisableFirstCleanup();
    InitCache();
}

void DiskCachePerfTest::CacheBackendPerformance()
{
    InitCache();
    EXPECT_TRUE(TimeWrite());

    disk_cache::SimpleBackendImpl::FlushWorkerPoolForTesting();
    base::RunLoop().RunUntilIdle();

    ResetAndEvictSystemDiskCache();
    EXPECT_TRUE(TimeRead(WhatToRead::HEADERS_ONLY,
        "Read disk cache headers only (cold)"));
    EXPECT_TRUE(TimeRead(WhatToRead::HEADERS_ONLY,
        "Read disk cache headers only (warm)"));

    disk_cache::SimpleBackendImpl::FlushWorkerPoolForTesting();
    base::RunLoop().RunUntilIdle();

    ResetAndEvictSystemDiskCache();
    EXPECT_TRUE(
        TimeRead(WhatToRead::HEADERS_AND_BODY, "Read disk cache entries (cold)"));
    EXPECT_TRUE(
        TimeRead(WhatToRead::HEADERS_AND_BODY, "Read disk cache entries (warm)"));

    disk_cache::SimpleBackendImpl::FlushWorkerPoolForTesting();
    base::RunLoop().RunUntilIdle();
}

TEST_F(DiskCachePerfTest, CacheBackendPerformance)
{
    CacheBackendPerformance();
}

TEST_F(DiskCachePerfTest, SimpleCacheBackendPerformance)
{
    SetSimpleCacheMode();
    CacheBackendPerformance();
}

int BlockSize()
{
    // We can use form 1 to 4 blocks.
    return (rand() & 0x3) + 1;
}

// Creating and deleting "entries" on a block-file is something quite frequent
// (after all, almost everything is stored on block files). The operation is
// almost free when the file is empty, but can be expensive if the file gets
// fragmented, or if we have multiple files. This test measures that scenario,
// by using multiple, highly fragmented files.
TEST_F(DiskCachePerfTest, BlockFilesPerformance)
{
    ASSERT_TRUE(CleanupCacheDir());

    disk_cache::BlockFiles files(cache_path_);
    ASSERT_TRUE(files.Init(true));

    int seed = static_cast<int>(Time::Now().ToInternalValue());
    srand(seed);

    const int kNumBlocks = 60000;
    disk_cache::Addr address[kNumBlocks];

    base::PerfTimeLogger timer1("Fill three block-files");

    // Fill up the 32-byte block file (use three files).
    for (int i = 0; i < kNumBlocks; i++) {
        EXPECT_TRUE(
            files.CreateBlock(disk_cache::RANKINGS, BlockSize(), &address[i]));
    }

    timer1.Done();
    base::PerfTimeLogger timer2("Create and delete blocks");

    for (int i = 0; i < 200000; i++) {
        int entry = rand() * (kNumBlocks / RAND_MAX + 1);
        if (entry >= kNumBlocks)
            entry = 0;

        files.DeleteBlock(address[entry], false);
        EXPECT_TRUE(
            files.CreateBlock(disk_cache::RANKINGS, BlockSize(), &address[entry]));
    }

    timer2.Done();
    base::RunLoop().RunUntilIdle();
}

} // namespace
